package com.bjsxt.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.alicp.jetcache.Cache;
import com.alicp.jetcache.anno.CreateCache;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bjsxt.domain.CashWithdrawAuditRecord;
import com.bjsxt.domain.CashWithdrawals;
import com.bjsxt.domain.Config;
import com.bjsxt.dto.SellGcnParamDto;
import com.bjsxt.dto.UserBankFeignDto;
import com.bjsxt.dto.UserDto;
import com.bjsxt.feign.UserBankFeignClient;
import com.bjsxt.feign.UserFeignClient;
import com.bjsxt.mapper.CashWithdrawalsMapper;
import com.bjsxt.modle.AccountVo;
import com.bjsxt.modle.CashWithdrawalsVo;
import com.bjsxt.service.AccountService;
import com.bjsxt.service.CashWithdrawAuditRecordService;
import com.bjsxt.service.CashWithdrawalsService;
import com.bjsxt.service.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
public class CashWithdrawalsServiceImpl extends ServiceImpl<CashWithdrawalsMapper, CashWithdrawals> implements CashWithdrawalsService {

    /**
     * 平台参数配置数据业务层
     */
    @Autowired
    private ConfigService configService;
    /**
     * 用户账户业务层
     */
    @Autowired
    private AccountService accountService;
    /**
     * userBank 远程调用对象
     */
    @Autowired
    private UserBankFeignClient userBankFeignClient;
    /**
     * 注入会员用户操作的 openFeign 对象
     */
    @Autowired
    private UserFeignClient userFeignClient;
    /**
     * redis 字符串操作对象
     */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 提现记录审核单业务层对象
     */
    @Autowired
    private CashWithdrawAuditRecordService cashWithdrawAuditRecordService;
    /**
     * JetCache 分布式锁对象
     * 缓存的前缀 CASH_RECHARGE_LOCK
     * 缓存存储时间 60s
     * 在缓存和本地都存放 cacheType = CacheType.BOTH
     */
    @CreateCache(name = "CASH_RECHARGE_LOCK:", expire = 60)
    private Cache<String, String> cache;

    /**
     * 根据条件分页查询 GCN 提现记录
     *
     * @param page      分页对象<CashWithdrawals>
     * @param userId    用户 ID
     * @param userName  用户名
     * @param mobile    手机号码
     * @param status    状态
     * @param numMin    最小充值金额
     * @param numMax    最大充值金额
     * @param startTime 开始日期
     * @param endTime   结束日期
     * @return 查询出的分页数据对象
     */
    @Override
    public Page<CashWithdrawalsVo> pageByUserIdAndUserNameAndMobileAndStatusAndNumAndCreateTime(Page<CashWithdrawals> page, Long userId, String userName, String mobile, Byte status, Integer numMin, Integer numMax, Date startTime, Date endTime) {
        // 查询出的用户数据，用于第一次进行条件的限定查询，三个值都没有则正常的走分页查询，再构建数据，否则则进行先查询 id 再进行数据的拼接，两个条件的限定查询只会走一次
        Map<Long, UserDto> userDtoMap = null;
        // 用户的 id 集合
        Set<Long> ids = null;
        // 判断，三个值有一个有值的情况才进行查询 id
        if (userId != null || !StringUtils.isEmpty(userName) || !StringUtils.isEmpty(mobile)) {
            // 将三个条件传递到会员系统进行查询，返回 map key 就是 id
            userDtoMap = userFeignClient.getBasicUsersMap(userId == null ? null : Collections.singletonList(userId), userName, mobile);
            // 用户 id 集合
            ids = userDtoMap.keySet();
        }
        // 构建查询多条件查询
        QueryWrapper<CashWithdrawals> queryWrapper = new QueryWrapper<>();
        // 构建查询条件
        queryWrapper.in(ids != null && !ids.isEmpty(), CashWithdrawals.COL_USER_ID, ids);
        queryWrapper.eq(status != null, CashWithdrawals.COL_STATUS, status);
        queryWrapper.between(numMin != null && numMax != null, CashWithdrawals.COL_NUM, numMin, numMax);
        queryWrapper.between(startTime != null && endTime != null, CashWithdrawals.COL_CREATED, startTime, endTime);
        // 调用方法进行数据的查询
        Page<CashWithdrawals> cashWithdrawalsPage = page(page, queryWrapper);
        // 如果在这里进行获取之后是空的没有获取到，那么就需要远程调用方法进行值的获取
        if (userDtoMap == null) {
            // 获取需要查询的 ids 集合
            List<Long> idList = cashWithdrawalsPage.getRecords().stream().map(CashWithdrawals::getUserId).collect(Collectors.toList());
            // 如果擦查询的 idList 是空则直接进行返回
            if (idList.isEmpty()) return new Page<>();
            // 根据 id 集合进行数据的查询
            userDtoMap = userFeignClient.getBasicUsersMap(idList, null, null);
        }
        // 如果擦查询的 userDtoMap 是空则直接进行返回
        if (userDtoMap.isEmpty()) return new Page<>();
        // 因为使用 lambda 表达式，下面这个 map 需要为 final 的
        Map<Long, UserDto> finalUserDtoMap = userDtoMap;
        // 拼接返回的 vo 对象，循环遍历返回的对象
        IPage<CashWithdrawalsVo> cashWithdrawalsVoIPage = cashWithdrawalsPage.convert(cashWithdrawals -> {
            // 创建转换的 vo 对象
            CashWithdrawalsVo cashWithdrawalsVo = new CashWithdrawalsVo();
            // 进行值的 copy
            BeanUtil.copyProperties(cashWithdrawals, cashWithdrawalsVo);
            // 获取用户对象
            UserDto userDto = finalUserDtoMap.get(cashWithdrawalsVo.getUserId());
            // 查询的数据不为空
            if (userDto != null) {
                // 设置昵称
                cashWithdrawalsVo.setUserName(userDto.getUsername());
                // 设置真实姓名
                cashWithdrawalsVo.setRealName(userDto.getRealName());
            }
            // 返回 vo 对象
            return cashWithdrawalsVo;
        });
        // 返回 page 对象
        return (Page<CashWithdrawalsVo>) cashWithdrawalsVoIPage;
    }

    /**
     * 根据用户 id 和审核状态查询 GCN 提现记录
     *
     * @param page   分页对象<CashWithdrawals>
     * @param userId 用户 id
     * @param status 状态：状态：0-待审核；1-审核通过；2-拒绝；3-提现成功；
     * @return 查询出的分页数据对象
     */
    @Override
    public Page<CashWithdrawals> pageByUserIdAndStatus(Page<CashWithdrawals> page, Long userId, Byte status) {
        // 调用查询方法并返回
        return page(page, new LambdaQueryWrapper<CashWithdrawals>().eq(CashWithdrawals::getUserId, userId).eq(status != null, CashWithdrawals::getStatus, status));
    }

    /**
     * GCN 货币的提现操作
     *
     * @param userId          用户 id
     * @param sellGcnParamDto 提现 GCN 传输类对象
     * @return true 提现成功 false 提现失败
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean sellGcn(Long userId, SellGcnParamDto sellGcnParamDto) {
        // 校验参数的合法性
        checkSellGcnParam(userId, sellGcnParamDto);
        // 查询用户的银行卡数据
        UserBankFeignDto userBank = userBankFeignClient.getUserBank(userId);
        if (userBank == null) throw new IllegalArgumentException("该用户下没有对应的银行卡数据，请绑定后重新提现。");
        // 生成参考号
        String remark = RandomUtil.randomNumbers(6);
        // 这里需要自己算提现金额，以免前端自己算的出现错误
        Config sellGcnRate = configService.getByCode("USDT2CNY");
        // 计算成交量（到账金额）
        BigDecimal mun = sellGcnParamDto.getNum().multiply(new BigDecimal(sellGcnRate.getValue())).setScale(2, RoundingMode.HALF_UP);
        // 获取最小的提现费用
        BigDecimal minPoundage = new BigDecimal(configService.getByCode("WITHDRAW_MIN_POUNDAGE").getValue());
        // 获取提现的费率
        BigDecimal poundageRate = new BigDecimal(configService.getByCode("WITHDRAW_POUNDAGE_RATE").getValue());
        // 计算提现手续费
        BigDecimal fee = sellGcnParamDto.getNum().multiply(poundageRate).setScale(2, RoundingMode.HALF_UP).max(minPoundage);
        // 获取资金账户id
        AccountVo accountVo = accountService.getByUserIdAndCoinName(userId, "GCN");
        // 创建提现的订单
        CashWithdrawals cashWithdrawals = new CashWithdrawals(null, userId, sellGcnParamDto.getCoinId(), accountVo.getId(), sellGcnParamDto.getNum(), fee, mun.subtract(fee), userBank.getRealName(), userBank.getBank(), userBank.getBankProv(), userBank.getBankCity(), userBank.getBankAddr(), userBank.getBankCard(), remark, (byte) 0, (byte) 0, null, null, null);
        // 保存数据
        boolean flag = save(cashWithdrawals);
        if (!flag) throw new RuntimeException("保存订单提现信息失败，请稍后再试");
        // 扣减用户的 GCN 余额
        return accountService.deductionAmount(userId, sellGcnParamDto.getCoinId(), cashWithdrawals.getMum(), fee, cashWithdrawals.getId(), "withdrawals_out");
    }

    /**
     * 现金提现审核
     *
     * @param userId                  用户 id
     * @param cashWithdrawAuditRecord 提现审核记录
     * @return true 审核成功 false 审核失败
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean cashRechargeAudit(Long userId, CashWithdrawAuditRecord cashWithdrawAuditRecord) {
        // 只能有一个员工进行审核，所以需要分布锁
        // 超时时间 300ms，300ms 没有释放会自动释放，锁的名称为 CASH_RECHARGE_LOCK:cashRechargeAuditRecord.id
        return cache.tryLockAndRun(String.valueOf(cashWithdrawAuditRecord.getId()), 300, TimeUnit.MILLISECONDS, () -> {
            // 获取充值记录单
            CashWithdrawals cashWithdrawals = getById(cashWithdrawAuditRecord.getId());
            // 如果查不到充值记录
            if (cashWithdrawals == null) throw new IllegalArgumentException("审核记录不存在");
                // 如果审核记录的状态为审核成功
            else if (cashWithdrawals.getStatus() == 1) throw new IllegalArgumentException("记录审核已经通过");
            // 新增一个审核记录单
            cashWithdrawAuditRecord.setAuditUserId(userId);
            cashWithdrawAuditRecord.setOrderId(cashWithdrawals.getId());
            cashWithdrawAuditRecord.setStep((byte) (cashWithdrawals.getStep() + 1));
            // 进行数据的保存
            boolean flag = cashWithdrawAuditRecordService.save(cashWithdrawAuditRecord);
            // 让下面的确定 flag 是 true 才进行后续操作
            Assert.isTrue(flag, "审核记录保存失败");
            // 如果没有审核通过，或者失败都需要做的操作，更换充值记录单状态
            cashWithdrawals.setStatus(cashWithdrawAuditRecord.getStatus());
            cashWithdrawals.setRemark(cashWithdrawAuditRecord.getRemark());
            // 设置审核次数
            cashWithdrawals.setStep(cashWithdrawAuditRecord.getStep());
            // 如果审核失败则进行直接的保存
            if (cashWithdrawals.getStatus() == 2) {
                // 进行数据的保存`
                flag = updateById(cashWithdrawals);
                // 让下面的确定 flag 是 true 才进行后续操作
                Assert.isTrue(flag, "审核记录保存失败");
            } else {
                // 进行金钱的转账
                flag = accountService.accountTransfer(userId, cashWithdrawals.getUserId(), cashWithdrawals.getCoinId(), cashWithdrawals.getId(), cashWithdrawals.getNum(), cashWithdrawals.getFee(), (byte) 2, "withdrawals_out", "提现");
                // 让下面的确定 flag 是 true 才进行后续操作
                Assert.isTrue(flag, "提现失败");
                // 设置最后确认到账时间
                cashWithdrawals.setLastTime(new Date());
                updateById(cashWithdrawals);
            }
        });
    }

    /**
     * 校验提现 GCN 传输类对象的合法性
     *
     * @param userId          用户 id
     * @param sellGcnParamDto 提现 GCN 传输类对象
     */
    private void checkSellGcnParam(Long userId, SellGcnParamDto sellGcnParamDto) {
        // 查看当前系统是否能进行提现
        String withdrawStatus = configService.getByCode("WITHDRAW_STATUS").getValue();
        if ("0".equals(withdrawStatus)) throw new RuntimeException("当前系统不允许进行提现操作，请稍后再试");
        // 获取提现最小的数量
        long minAmount = Long.valueOf(configService.getByCode("WITHDRAW_MIN_AMOUNT").getValue());
        if (minAmount > sellGcnParamDto.getNum().longValue())
            throw new IllegalArgumentException("小于最小提现的金额，最小提现金额为" + minAmount);
        // 获取提现最大的数量
        long maxAmount = Long.valueOf(configService.getByCode("WITHDRAW_MAX_AMOUNT").getValue());
        if (maxAmount < sellGcnParamDto.getNum().longValue())
            throw new IllegalArgumentException("大于最大提现的金额，最大提现金额为" + maxAmount);
        // 获取每日最大的提现数量
        long dayMaxAmount = Long.valueOf(configService.getByCode("WITHDRAW_DAY_MAX_AMOUNT").getValue());
        // 校验手机的验证码
        List<UserDto> users = userFeignClient.getBasicUsers(Collections.singletonList(userId));
        if (users == null || users.isEmpty()) throw new IllegalArgumentException("用户不存在");
        // 获取用户手机号，根据手机号获取验证码数据
        String validateCode = stringRedisTemplate.opsForValue().get("SMS:CASH_WITHDRAWS:" + users.get(0).getMobile());
        if (!sellGcnParamDto.getValidateCode().equals(validateCode)) throw new IllegalArgumentException("验证码不正确");
        // 校验用户的密码
        boolean flag = BCrypt.checkpw(sellGcnParamDto.getPayPassword(), users.get(0).getPaypassword());
        if (!flag) throw new IllegalArgumentException("支付密码不正确");
    }
}
